/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.kernel;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import fr.x9c.cadmium.util.IO;
import fr.x9c.cadmium.util.Misc;
import fr.x9c.cadmium.util.Stack;

/**
 * This class implements the methods needed to debug the program currently
 * interpreted by Cadmium.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.3
 * @since 1.3
 */
public final class Debugger {

    /**
     * This enum defines the various kinds of events reported by the interpreter
     * to the debugger.
     */
    public static enum EventKind {

        /** The interpreted program exhausted the requested number of events. */
        EVENT_COUNT,

            /** The interpreted program reached a breakpoint. */
            BREAKPOINT,

            /** The interpreted program just started. */
            PROGRAM_START,

            /** The interpreted program exited. */
            PROGRAM_EXIT,

            /** The interpreted program "crossed the barrier" at exception raise. */
            TRAP_BARRIER,

            /** The interpreted program raised an uncaught exception. */
            UNCAUGHT_EXC

    } // end enum 'EventKind'

    /** Map from character codes to requests. */
    private static final Map<Integer, Request> requestMap = new HashMap<Integer, Request>();

    /** Map from character codes to replies. */
    private static final Map<Integer, Reply> replyMap = new HashMap<Integer, Reply>();

    /**
     * This enum defines the kinds of messages to be sent by the debugger to debuggee.
     */
    public static enum Request {

        /** The debugger wants to set an event. */
        REQ_SET_EVENT('e'),

            /** The debugger wants to set a breakpoint. */
            REQ_SET_BREAKPOINT('B'),

            /** The debugger wants to unset an event/breakpoint. */
            REQ_RESET_INSTR('i'),

            /** The debugger wants to set a checkpoint. */
            REQ_CHECKPOINT('c'),

            /** The debugger wants to run a given number of events. */
            REQ_GO('g'),

            /** The debugger wants to stop the program. */
            REQ_STOP('s'),

            /** The debugger wants to stop the process associated with a checkpoint. */
            REQ_WAIT('w'),

            /** The debugger wants to set the current frame to the top one. */
            REQ_INITIAL_FRAME('0'),

            /** The debugger wants to get the current frame. */
            REQ_GET_FRAME('f'),

            /** The debugger wants to set the current frame to a given offset. */
            REQ_SET_FRAME('S'),

            /** The debugger wants to move the current frame from one up. */
            REQ_UP_FRAME('U'),

            /** The debugger wants to set the trap barrier. */
            REQ_SET_TRAP_BARRIER('b'),

            /** The debugger wants to get a given local value. */
            REQ_GET_LOCAL('L'),

            /** The debugger wants to get a given element from the environment. */
            REQ_GET_ENVIRONMENT('E'),

            /** The debugger wants to get a given element from the globals. */
            REQ_GET_GLOBAL('G'),

            /** The debugger wants to get the accumulator. */
            REQ_GET_ACCU('A'),

            /** The debugger wants to get the header of a value. */
            REQ_GET_HEADER('H'),

            /** The debugger wants to get one given field of a value. */
            REQ_GET_FIELD('F'),

            /** The debugger wants to get a copy of a value. */
            REQ_MARSHAL_OBJ('M'),

            /** The debugger wants to get the offset of a given closure. */
            REQ_GET_CLOSURE('C');

        /** Character code associated with the request. */
        private final int code;

        /**
         * Constructs a request from a character code. <br/>
         * The <i>requestMap</i> map is also updated.
         * @param c character code for request
         */
        private Request(final char c) {
            final int x = Misc.signedToUnsignedByte(Misc.convertCharToByte(c));
            this.code = x;
            Debugger.requestMap.put(x, this);
        } // end constructor(char)

        /**
         * Returns the request corresponding to the passed character code.
         * @param c character code to get request for
         * @return the request corresponding to the passed character code,
         *         <tt>null</tt> if such a reply does not exist
         */
        public static Request fromCharCode(final int c) {
            final Request res = Debugger.requestMap.get(c);
            assert res != null : "invalid code";
            return res;
        } // end method 'fromCharCode(int)'

        /**
         * Returns the character code of the request.
         * @return the character code of the request
         */
        public int getCharCode() {
            return this.code;
        } // end method 'getCharCode()'

    } // end enum 'Request'

    /**
     * This enum defines the kinds of messages to be sent by the debuggee to debugger.
     */
    public static enum Reply {

        /** The interpreted program exhausted the requested number of events. */
        REP_EVENT('e'),

            /** The interpreted program reached a breakpoint. */
            REP_BREAKPOINT('b'),

            /** The interpreted program exited. */
            REP_EXITED('x'),

            /** The interpreted program "crossed the barrier" at exception raise. */
            REP_TRAP('s'),

            /** The interpreted program raised an uncaught exception. */
            REP_UNCAUGHT_EXC('u');

        /** Character code associated with the reply. */
        private final int code;

        /**
         * Constructs a reply from a character code. <br/>
         * The <i>replyMap</i> map is also updated.
         * @param c character code for reply
         */
        private Reply(final char c) {
            final int x = Misc.signedToUnsignedByte(Misc.convertCharToByte(c));
            this.code = x;
            Debugger.replyMap.put(x, this);
        } // end constructor(char)

        /**
         * Returns the reply corresponding to the passed character code.
         * @param c character code to get reply for
         * @return the reply corresponding to the passed character code,
         *         <tt>null</tt> if such a reply does not exist
         */
        public static Reply fromCharCode(final int c) {
            final Reply res = Debugger.replyMap.get(c);
            assert res != null : "invalid code";
            return res;
        } // end method 'fromCharCode(int)'

        /**
         * Returns the character code of the reply.
         * @return the character code of the reply
         */
        public int getCharCode() {
            return this.code;
        } // end method 'getCharCode()'

    } // end enum 'Reply'

    /**
     * No instance of this class.
     */
    private Debugger() {
    } // end empty constructor

    /**
     * Initializes the connection with the debugger if the environment variable
     * <tt>"CAML_DEBUG_SOCKET"</tt> is set to a <i>host</i><tt>:</tt><i>port</i> value. <br/>
     * The passed context is updated on successful connection.
     * @param ctxt context - should not be <tt>null</tt>
     * @throws Fatal.Exception if the <tt>"CAML_DEBUG_SOCKET"</tt> environment variable
     *                         do no contain a <i>port</i> component
     */
    public static void init(final Context ctxt) throws Fatal.Exception {
        assert ctxt != null : "null ctxt";

        // decode address
        final String address;
        try {
            address = System.getenv("CAML_DEBUG_SOCKET");
        } catch (final SecurityException t) {
            return;
        } // end try/catch
        if (address == null) {
            return;
        } // end if
        final int colonIndex = address.indexOf(":");
        final String port = colonIndex >= 0 ? address.substring(colonIndex + 1) : null;
        final String host = colonIndex >= 0 ? address.substring(0, colonIndex) : address;
        if (port == null) {
            Fatal.raise("Unix socket not supported");
            return; // never reached
        } // end if

        // actually open connection
        final Socket socket;
        try {
            socket = new Socket(host, Integer.parseInt(port));
            final DataOutputStream out = new DataOutputStream(socket.getOutputStream());
            if (!ctxt.isDebuggerInUse()) {
                IO.write32s(out, -1);
            } // end if
            // should write pid (unsigned 32-bit int)
            IO.write32u(out, 0);
            out.flush();
        } catch (final Exception e) {
            final String msg = String.format("cannot connect to debugger at %s\nerror: %s",
                                             address,
                                             e.getMessage());
            Fatal.raise(msg);
            return;
        } // end try/catch

        // update context
        ctxt.setDebuggerInUse(true);
        ctxt.setDebuggerSocket(socket);
        ctxt.setDebuggerTrapBarrier(0);
    } // end method 'init(Context)'

    /**
     * Handles an event of the interpreted program; then executes the commands sent
     * by the debugger.
     * @param runner context - should not be <tt>null</tt>
     * @param event program event to handle - should not be <tt>null</tt>
     * @throws Fatal.Exception if an unssupported feature is called (checkpoint, wait)
     * @throws Fatal.Exception if an i/o error occurs
     * @throws FalseExit if program exits (and exit is set not to shutdown JVM)
     * @throws Fail.Exception if any other error occurs (<i>e.g.</i> stack error)
     */
    @SuppressWarnings("fallthrough")
    public static void handleEvent(final ByteCodeRunner runner, final EventKind event)
        throws Fail.Exception, Fatal.Exception, FalseExit {
        assert runner != null : "null runner";
        assert event != null : "null event";
        final Context ctxt = runner.getContext();
        final Socket socket = ctxt.getDebuggerSocket();
        if (socket == null) {
            return;
        } // end if
        final DataInputStream in;
        final DataOutputStream out;
        try {
            in = new DataInputStream(socket.getInputStream());
            out = new DataOutputStream(socket.getOutputStream());
        } catch (final IOException ioe) {
            // the socket has been closed
            return;
        } // end try/catch
        try {
            final List<Value> values = new ArrayList<Value>();
            final Stack stack = runner.getStack();
            final int stackSize = stack.size();
            int frame = stackSize - 2;
            switch (event) {
            case PROGRAM_START:
                break;
            case EVENT_COUNT:
                IO.write8u(out, Reply.REP_EVENT.getCharCode());
                break;
            case BREAKPOINT:
                IO.write8u(out, Reply.REP_BREAKPOINT.getCharCode());
                break;
            case PROGRAM_EXIT:
                IO.write8u(out, Reply.REP_EXITED.getCharCode());
                break;
            case TRAP_BARRIER:
                IO.write8u(out, Reply.REP_TRAP.getCharCode());
                break;
            case UNCAUGHT_EXC:
                IO.write8u(out, Reply.REP_UNCAUGHT_EXC.getCharCode());
                break;
            default:
                assert false : "invalid event";
                break;
            } // end switch
            if (event != EventKind.PROGRAM_START) {
                IO.write32u(out, ctxt.getDebuggerEventCount());
                if ((event == EventKind.EVENT_COUNT) || (event == EventKind.BREAKPOINT)) {
                    IO.write32s(out, frame);
                    IO.write32s(out, stack.peek(1).asCodeOffset() * 4);
                } else {
                    IO.write32s(out, 0);
                    IO.write32s(out, 0);
                } // end if/else
                out.flush();
            } // end if
            final int[] code = ctxt.getCode();
            final int[] savedCode = ctxt.getSavedCode();
            final int codeLen = code.length;
            int pos;
            Value val;
            Block blk;
            while (true) {
                final int command = in.read();
                if (command == -1) {
                    return;
                } // end if
                final Request req = Request.fromCharCode(command);
                switch (req) {
                case REQ_SET_EVENT:
                    pos = IO.read32s(in) / 4;
                    assert (pos >= 0) && (pos < codeLen) : "invalid code position";
                    code[pos] = Instructions.EVENT;
                    break;
                case REQ_SET_BREAKPOINT:
                    pos = IO.read32s(in) / 4;
                    assert (pos >= 0) && (pos < codeLen) : "invalid code position";
                    code[pos] = Instructions.BREAK;
                    break;
                case REQ_RESET_INSTR:
                    pos = IO.read32s(in) /  4;
                    assert (pos >= 0) && (pos < codeLen) : "invalid code position";
                    code[pos] = savedCode[pos];
                    break;
                case REQ_CHECKPOINT:
                    Fatal.raise("error: REQ_CHECKPOINT command");
                    return; // never reached
                case REQ_GO:
                    ctxt.setDebuggerEventCount(IO.read32u(in));
                    return;
                case REQ_STOP:
                    if (ctxt.getParameters().isExitStoppingJVM()) {
                        System.exit(0);
                    } else {
                        ctxt.getThreadGroup().interrupt();
                        ctxt.interruptAdditionalThreads();
                        throw new FalseExit(ctxt, 0);
                    } // end if/else
                    return; // never reached
                case REQ_WAIT:
                    Fatal.raise("error: REQ_WAIT command");
                    return; // never reached
                case REQ_INITIAL_FRAME:
                    frame = stackSize - 2;
                    // intentional fall through
                case REQ_GET_FRAME:
                    IO.write32s(out, frame);
                    if (frame > 0) {
                        IO.write32s(out, stack.peek(1).asCodeOffset() * 4);
                    } else {
                        IO.write32s(out, 0);
                    } // end if/else
                    out.flush();
                    break;
                case REQ_SET_FRAME:
                    pos = IO.read32s(in);
                    frame = stackSize - pos;
                    break;
                case REQ_UP_FRAME: {
                    pos = IO.read32s(in);
                    final int extraArgs = stack.peek((stackSize - 2 - frame) + 3).asLong();
                    if (frame - extraArgs - pos - 3 <= 0) {
                        IO.write32s(out, -1);
                    } else {
                        frame -= (extraArgs + pos + 3);
                        IO.write32s(out, frame);
                        IO.write32s(out, stack.peek(stackSize - 2 - frame + 1).asCodeOffset() * 4);
                    } // end if/else
                    break;
                }
                case REQ_SET_TRAP_BARRIER:
                    pos = IO.read32s(in);
                    ctxt.setDebuggerTrapBarrier(pos);
                    break;
                case REQ_GET_LOCAL:
                    pos = IO.read32s(in);
                    putVal(out, values, stack.peek((stackSize - 2 - frame) + 4 + pos));
                    out.flush();
                    break;
                case REQ_GET_ENVIRONMENT:
                    pos = IO.read32s(in);
                    putVal(out, values, stack.peek((stackSize - 2 - frame) + 2).asBlock().get(pos));
                    out.flush();
                    break;
                case REQ_GET_GLOBAL:
                    pos = IO.read32s(in);
                    putVal(out, values, ctxt.getGlobalData().asBlock().get(pos));
                    out.flush();
                    break;
                case REQ_GET_ACCU:
                    putVal(out, values, stack.peek(0));
                    out.flush();
                    break;
                case REQ_GET_HEADER:
                    val = getVal(in, values);
                    IO.write32s(out, val.asBlock().getHeader());
                    out.flush();
                    break;
                case REQ_GET_FIELD:
                    val = getVal(in, values);
                    blk = val.asBlock();
                    pos = IO.read32s(in);
                    if (blk.getTag() != Block.DOUBLE_ARRAY_TAG) {
                        IO.write8s(out, 0);
                        putVal(out, values, blk.get(pos));
                    } else {
                        IO.write8s(out, 1);
                        IO.write64s(out, Double.doubleToRawLongBits(blk.getDouble(pos)));
                    } // end if/else
                    out.flush();
                    break;
                case REQ_MARSHAL_OBJ:
                    val = getVal(in, values);
                    out.write(Extern.externValue(ctxt, val, Value.UNIT));
                    out.flush();
                    break;
                case REQ_GET_CLOSURE:
                    val = getVal(in, values);
                    blk = val.asBlock();
                    IO.write32s(out, blk.get(0).asCodeOffset() * 4);
                    out.flush();
                    break;
                default:
                    assert false : "invalid request";
                    break;
                } // end switch
            } // end while
        } catch (final IOException ioe) {
            Fatal.raise("error while communicating with the debugger");
        } // end try/catch
    } // end method 'handleEvent(ByteCodeRunner, EventKind)'

    /**
     * Sends the <i>pointer</i> corresponding to a value.
     * @param out where to send value - should not be <tt>null</tt>
     * @param l list of sent values - should not be <tt>null</tt>
     * @param v value to send - should not be <tt>null</tt>
     * @throws IOException if an i/o error occurs
     */
    private static void putVal(final DataOutputStream out,
                               final List<Value> l,
                               final Value v)
        throws IOException {
        if (v.isBlock()) {
            final int len = l.size();
            int index = 0;
            while ((index < len) && (l.get(index) != v)) {
                index++;
            } // end while
            if (index < len) {
                IO.write32s(out, index << 1);
            } else {
                l.add(v);
                IO.write32s(out, len << 1);
            } // end if/else
        } else {
            IO.write32s(out, v.getRawValue());
        } // end if/else
    } // end method 'putVal(DataOutputStream, List<Value>, Value)'

    /**
     * Reads a <i>pointer</i> value.
     * @param in where to read value from - should not be <tt>null</tt>
     * @param l list of sent values - should not be <tt>null</tt>
     * @return the read value
     * @throws Fatal.Exception if the read value is invalid
     * @throws IOException if an i/o error occurs
     * @throws Fatal.Exception if the read <i>pointer</i> is unknown
     */
    private static Value getVal(final DataInputStream in,
                                final List<Value> l)
        throws Fatal.Exception, IOException {
        final int v = IO.read32s(in);
        if ((v & 1) == 0) {
            final int idx =  v >> 1;
            if ((idx >= 0) && (idx < l.size())) {
                return l.get(idx);
            } else {
                Fatal.raise("debugger: unknown value");
                return null; // never reached
            } // end if/else
        } else {
            return Value.createFromRawValue(v);
        } // end if/else
    } // end method 'getVal(DataInputStream, List<Value>)'

} // end class 'Debugger'
